package com.caishi.lkx.common.config;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import com.zzw.common.AliyunConfig;
import com.zzw.common.SpringContextHolder;
import com.zzw.common.file.CommonOssConfig;
import com.zzw.common.file.OssPlugins;
import com.zzw.common.lock.RedissonLock;
import com.zzw.common.mysql.LogicalDataMapper;
import com.zzw.common.threadpool.SysThreadPool;
import com.zzw.common.utils.TimeUtil;
import com.zzw.common.utils.UUIDUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.support.CronTrigger;

import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;


/**
 * @author by keray
 * date:2021/4/17 10:57 上午
 */
//@Component
@Slf4j
public class LogicalDataArchiveTask {

    private final String uuid = UUIDUtil.generateUUIDByTimestamp();

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Resource
    private ApplicationContext applicationContext;

    @Value("${spring.application.name}")
    private String applicationName;

    @Value("${spring.profiles.active}")
    private String env;

    private final OssPlugins ossPlugins;

    private final List<String> myManger = new LinkedList<>();

    public LogicalDataArchiveTask(AliyunConfig aliyunConfig, CommonOssConfig ossConfig) {
        CommonOssConfig config = ossConfig.copy();
        config.setBucket(ossConfig.getDiskBucket());
        this.ossPlugins = new OssPlugins(aliyunConfig, config, redisTemplate) {
            @Override
            public String getFilePath(String fileName) {
                return fileName;
            }
        };
    }

    @EventListener
    public void scheduleInit(ApplicationStartedEvent startedEvent) {
        applicationContext.getBeansOfType(LogicalDataMapper.class).forEach((k, v) -> {
            try {
                submitTask(k);
            } catch (Exception e) {
                log.error("提交逻辑数据删除任务失败", e);
            }
        });
        SysThreadPool.taskScheduler.schedule(this::heartbeat, new CronTrigger("0 0 0/1 * * ?"));
    }

    // 心跳、
    public void heartbeat() {
        redisTemplate.opsForValue().set(heartbeatPrefix() + uuid, TimeUtil.DATE_TIME_FORMATTER_SC.format(LocalDateTime.now()));
        AtomicInteger expireTask = new AtomicInteger(0);
        Objects.requireNonNull(redisTemplate.keys(heartbeatPrefix() + "*"))
                .parallelStream()
                .forEach(key -> {
                    LocalDateTime lastTime = LocalDateTime.parse(redisTemplate.opsForValue().get(key), TimeUtil.DATE_TIME_FORMATTER_SC);
                    // 设备掉线了
                    if (lastTime.isBefore(LocalDateTime.now().minusHours(1).minusMinutes(10))) {
                        String deviceId = key.split(":")[2];
                        log.info("设备掉线：{}", deviceId);
                        // 移除设备设置的定时任务
                        Objects.requireNonNull(redisTemplate.keys(key("*")))
                                .parallelStream().forEach(beanTask -> {
                                    String settingDeviceId = redisTemplate.opsForValue().get(beanTask);
                                    if (deviceId.equals(settingDeviceId)) {
                                        log.info("取消任务绑定：{} {}", deviceId, beanTask);
                                        redisTemplate.delete(beanTask);
                                        expireTask.getAndIncrement();
                                    }
                                });
                        redisTemplate.delete(key);
                    }
                });
        if (expireTask.get() > 0) {
            scheduleInit(null);
        }
    }

    private void submitTask(String beanName) throws InterruptedException, TimeoutException {
        log.info("提交逻辑删除检查任务：{}", beanName);
        RedissonLock.lock("com.caishi.lkx.common.config.LogicalDataArchiveTask" + beanName, k -> {
            if (Boolean.TRUE.equals(redisTemplate.hasKey(key(beanName)))) {
                return;
            }
            String cron = String.format("0 0 %s %s * ? ", (Math.abs(beanName.hashCode()) % 7 + 1), (Math.abs(beanName.hashCode()) % 28 + 1));
            log.info("提交逻辑数据清理任务 cron={}", cron);
            SysThreadPool.taskScheduler.schedule(() -> dataDelete(beanName), new CronTrigger(cron));
            redisTemplate.opsForValue().set(key(beanName), uuid);
            myManger.add(key(beanName));
            log.info("提交逻辑删除检查任务成功：{}", beanName);
        });
    }

    // 清理逻辑删除数据
    private void dataDelete(String beanName) {
        String key = key(beanName);
        String settingUUID = redisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(settingUUID)) {
            try {
                submitTask(beanName);
            } catch (InterruptedException | TimeoutException e) {
                log.error("容错提交逻辑数据删除任务失败", e);
            }
            return;
        }
        if (!settingUUID.equals(uuid)) {
            return;
        }
        LogicalDataMapper<?, ?> dataMapper = SpringContextHolder.getBean(beanName);
        List<Serializable> ids = new LinkedList<>();
        File file = readDeleteData(beanName, dataMapper, ids);
        try {
            if (file != null && CollUtil.isNotEmpty(ids)) {
                String tableName = SqlHelper.table(dataMapper.currentModelClass()).getTableName();
                upload(new FileInputStream(file), tableName);
                log.warn("物理删除表{}的数据，ids={}", tableName, ids);
                sqlDeleteData(dataMapper, ids);
            }
        } catch (IOException e) {
            log.error("逻辑删除失败", e);
        } finally {
            if (file != null) {
                file.delete();
            }
        }
    }

    private File readDeleteData(String beanName, LogicalDataMapper<?, ?> dataMapper, List<Serializable> ids) {
        File file = new File(String.format("./%s-tmp.csv", beanName));
        if (file.exists()) {
            file.delete();
        }
        try {
            if (file.createNewFile()) {
                try (SqlSession session = SqlHelper.sqlSession(dataMapper.currentModelClass())) {
                    TableInfo tableInfo = SqlHelper.table(dataMapper.currentModelClass());
                    String sql = String.format("select * from %s where deleted=1 limit 10000", tableInfo.getTableName());
                    Statement statement = session.getConnection().createStatement();
                    ResultSet resultSet = statement.executeQuery(sql);
                    ResultSetMetaData rsmd = resultSet.getMetaData();
                    int columnCount = rsmd.getColumnCount();
                    FileOutputStream outputStream = new FileOutputStream(file);
                    try {
                        while (resultSet.next()) {
                            for (int i = 1; i <= columnCount; i++) {
                                String data = resultSet.getString(i);
                                if (i == 1) {
                                    ids.add(data);
                                }
                                outputStream.write(data.getBytes(StandardCharsets.UTF_8));
                                if (i < columnCount) {
                                    outputStream.write(",".getBytes(StandardCharsets.UTF_8));
                                }
                            }
                            outputStream.write("\n".getBytes(StandardCharsets.UTF_8));
                        }
                    } finally {
                        outputStream.flush();
                        outputStream.close();
                    }
                    statement.close();
                    return file;
                } catch (Exception e) {
                    log.error("数据删除失败", e);
                }
            }
        } catch (IOException e) {
            log.error("文件创建失败", e);
        }
        return null;
    }

    private void upload(InputStream inputStream, String tableName) throws IOException {
        String fileName = ossPlugins.streamUploadSync(inputStream, String.format("disk/mysqlDeleteData/%s/%s/%s/%s.csv", env, applicationName, tableName, TimeUtil.DATE_TIME_FORMATTER_SN.format(LocalDateTime.now())));
        inputStream.close();
        log.info("逻辑删除数据备份完成：{}", fileName);
    }

    private void sqlDeleteData(LogicalDataMapper<?, ?> dataMapper, List<Serializable> ids) {
        try (SqlSession session = SqlHelper.sqlSession(dataMapper.currentModelClass())) {
            TableInfo tableInfo = SqlHelper.table(dataMapper.currentModelClass());
            String sql = String.format("DELETE from %s where id in (%s)", tableInfo.getTableName(), ids.stream().map(id -> {
                if (id instanceof String) {
                    return String.format("'%s'", id);
                }
                return id.toString();
            }).collect(Collectors.joining(",")));
            System.out.println("de:" + sql);
            Statement statement = session.getConnection().createStatement();
            statement.execute(sql);
            session.commit();
            statement.close();
        } catch (Exception e) {
            log.error("sql数据删除失败", e);
        }
    }

    private String key(String beanName) {
        return String.format("LogicalDataArchiveTask:%s:%s", applicationName, beanName);
    }

    private String heartbeatPrefix() {
        return String.format("LogicalDataArchiveTask:%s:heartbeat:", applicationName);
    }


    @PreDestroy
    public void destroy() {
        myManger.parallelStream().forEach(key -> redisTemplate.delete(key));
    }

}
